/*
 *    Copyright 2008-2009 The Digester Tools Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.digestertools.annotations;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import lombok.Getter;
import lombok.Setter;

import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.digester.RuleSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.digestertools.annotations.reflect.MethodArgument;
import com.digestertools.annotations.utils.AnnotationUtils;

/**
 * 
 * @author Simone Tripodi
 * @version $Id: FromAnnotationsRuleSet.java 378 2009-12-17 15:21:22Z simone.tripodi $
 */
public final class FromAnnotationsRuleSet implements RuleSet {

    private static final Map<Class<?>, AnnotationHandler<? extends Annotation, ? extends AnnotatedElement>> HANDLERS =
        new HashMap<Class<?>, AnnotationHandler<? extends Annotation,? extends AnnotatedElement>>();

    private final Log log = LogFactory.getLog(this.getClass());

    private final Map<String, List<AnnotationRuleProvider<? extends Rule>>> rules
        = new LinkedHashMap<String, List<AnnotationRuleProvider<? extends Rule>>>();

    @Getter
    @Setter
    private String namespaceURI;

    /**
     * {@inheritDoc}
     */
    public void addRuleProvider(String pattern, AnnotationRuleProvider<? extends Rule> ruleProvider) {
        if (this.rules.containsKey(pattern)) {
            this.rules.get(pattern).add(ruleProvider);
        } else {
            List<AnnotationRuleProvider<? extends Rule>> rules = new ArrayList<AnnotationRuleProvider<? extends Rule>>();
            rules.add(ruleProvider);
            this.rules.put(pattern, rules);
        }
    }

    /**
     * 
     * @param <T>
     * @param pattern
     * @param providerClass
     * @return
     */
    public <T extends AnnotationRuleProvider<? extends Rule>> T getProvider(String pattern, Class<T> providerClass) {
        if (!this.rules.containsKey(pattern)) {
            return null;
        }

        for (AnnotationRuleProvider<? extends Rule> rule : this.rules.get(pattern)) {
            if (providerClass.isInstance(rule)) {
                return providerClass.cast(rule);
            }
        }

        return null;
    }

    public void addRules(final FromAnnotationsRuleSet ruleSet) {
        this.rules.putAll(ruleSet.getRules());
    }

    private Map<String, List<AnnotationRuleProvider<? extends Rule>>> getRules() {
        return this.rules;
    }

    /**
     * {@inheritDoc}
     */
    public void addRulesFrom(Class<?> target, AnnotationsContext context) {
        if (this.log.isDebugEnabled()) {
            this.log.debug("Analyzing class '"
                    + target.getName()
                    + "'");
        }

        if (target == Object.class
                || target.isInterface()
                || Modifier.isAbstract(target.getModifiers())) {
            if (this.log.isDebugEnabled()) {
                this.log.debug("Class '"
                        + target.getName()
                        + "' is interface or abstract or Object, analysis is over");
            }

            return;
        }

        for (Annotation annotation : target.getAnnotations()) {
            this.handle(annotation, target, context);
        }

        // class fields
        for (Field field : target.getDeclaredFields()) {
            for (Annotation annotation : field.getAnnotations()) {
                this.handle(annotation, field, context);
            }
        }

        // class methods
        for (Method method : target.getDeclaredMethods()) {
            for (Annotation annotation : method.getAnnotations()) {
                this.handle(annotation, method, context);
            }

            // method args
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            Class<?>[] parameterTypes = method.getParameterTypes();
            MethodArgument methodArgument;
            for (int i = 0; i < parameterTypes.length; i++) {
                methodArgument = new MethodArgument(i, parameterTypes[i], parameterAnnotations[i], method);

                for (int j = 0; j < parameterAnnotations[i].length; j++) {
                    this.handle(parameterAnnotations[i][j], methodArgument, context);
                }
            }
        }

        this.addRulesFrom(target.getSuperclass(), context);
    }

    /**
     * {@inheritDoc}
     */
    public void addRuleInstances(Digester digester) {
        String pattern;
        Rule rule;
        for (Entry<String, List<AnnotationRuleProvider<? extends Rule>>> entry : this.rules.entrySet()) {
            pattern = entry.getKey();
            for (AnnotationRuleProvider<? extends Rule> provider : entry.getValue()) {
                rule = provider.newRule();
                rule.setNamespaceURI(this.namespaceURI);

                if (this.log.isDebugEnabled()) {
                    this.log.debug("digester.addRule("
                            + pattern
                            + ", "
                            + rule
                            + ");");
                }

                digester.addRule(pattern, rule);
            }
        }
    }

    /**
     * 
     * @param annotation
     * @return
     */
    @SuppressWarnings("unchecked")
    private <A extends Annotation, E extends AnnotatedElement> void handle(A annotation,
            E target,
            AnnotationsContext context) {
        // if it is a digester annoation, process it
        if (annotation.annotationType().isAnnotationPresent(DigesterRule.class)) {
            if (this.log.isDebugEnabled()) {
                this.log.debug("Processing digester rule annotation '"
                        + annotation
                        + "'");
            }

            DigesterRule digesterRule =
                (DigesterRule) annotation.annotationType().getAnnotation(DigesterRule.class);
            Class<? extends AnnotationHandler<? extends Annotation, ? extends AnnotatedElement>> clazz =
                digesterRule.handledBy();

            if (this.log.isDebugEnabled()) {
                this.log.debug("Looking for handler of type '"
                        + clazz.getName()
                        + "'");
            }

            AnnotationHandler<A, E> handler = null;

            if (HANDLERS.containsKey(clazz)) {
                if (this.log.isDebugEnabled()) {
                    this.log.debug("Handler of type '"
                            + clazz.getName()
                            + "' already created, retrieving it from cache");
                }

                handler = (AnnotationHandler<A, E>) HANDLERS.get(clazz);
            } else {
                if (this.log.isDebugEnabled()) {
                    this.log.debug("Handler of type '"
                            + clazz.getName()
                            + "' has to be created and stored to the cache");
                }

                try {
                    handler = (AnnotationHandler<A, E>) clazz.newInstance();
                    HANDLERS.put(clazz, handler);
                } catch (Exception e) {
                    if (this.log.isWarnEnabled()) {
                        this.log.warn("Impossible to create handler of type '"
                            + clazz.getName()
                            + "' using the default empty constructor", e);
                    }
                    return;
                }
            }

            handler.handle(annotation, target, context, this);
        } else {
            // check if it is one of the @*.List annotation
            if (this.log.isDebugEnabled()) {
                this.log.debug("Annotation '"
                        + annotation.annotationType().getName()
                        + "' is not annotated by '@"
                        + DigesterRule.class.getName()
                        + "', checking it is a list of digester annotations");
            }

            Annotation[] annotations = AnnotationUtils.getAnnotationsArrayValue(annotation);
            if (annotations != null) {
                // if it is an annotations array, process them
                for (Annotation ptr : annotations) {
                    this.handle(ptr, target, context);
                }
            } else if (this.log.isDebugEnabled()) {
                this.log.debug("Expected annotations array for value() Annotation '"
                        + annotation.annotationType().getName()
                        + "' or is null, ignoring it");
            }
        }
    }

    @Override
    public String toString() {
        return this.rules.toString();
    }

}
